Skip to Content

1. 为什么需要 App Router?(解决了什么问题)

在 App Router 之前,Next.js 使用的是 Pages Router(页面路由)。尽管 Pages Router 非常成功和易于上手,但它也存在一些局限性:

  1. 布局(Layout)的限制:在 Pages Router 中,实现嵌套布局(例如,一个 dashboard 页面,其侧边栏和顶部导航栏保持不变,只有主内容区域变化)通常需要一些变通方法,比如在 _app.js 中使用 getLayout 属性,不够直观和灵活。
  2. 数据获取的僵化:数据获取与页面强绑定,主要通过 getServerSideProps, getStaticProps, 和 getStaticPaths 这几个特定的函数。你无法在组件层级灵活地获取数据,导致所有数据都必须在页面级别加载,有时会造成瀑布流(waterfall)加载问题。
  3. 客户端 JavaScript 过多:默认情况下,Pages Router 中的页面组件最终都会在客户端进行水合(hydrate),即使页面大部分是静态的。这意味着随着应用变大,发送到客户端的 JavaScript 包体积也会随之增长,影响初始加载性能。
  4. 状态管理的复杂性:在页面切换时,全局状态(如 _app.js 中的 state)会保留,但布局本身可能会被重新渲染,状态管理不够优雅。

App Router 的设计初衷就是为了解决以上这些痛点。


2. App Router 的核心概念

要理解 App Router,必须先掌握以下几个核心概念:

a. React Server Components (RSC)

这是 App Router 的基石。

  • 什么是 RSC?:这是一种新型的 React 组件,它 只在服务器上执行和渲染。它的渲染结果(一种特殊的 JSON 格式,不是 HTML)被流式传输到客户端,客户端 React 能够理解并用它来更新 DOM。
  • 关键优势
    • 零客户端包体积:默认情况下,Server Components 的代码不会被打包进客户端的 JavaScript 文件中。这意味着你的组件,即使包含大量依赖(如 lodash, date-fns),也不会增加用户的下载负担。
    • 直接访问后端资源:由于它们在服务器上运行,你可以直接在组件中执行数据库查询、文件系统操作或调用内部 API,而无需创建额外的 API 路由。这极大地简化了数据获取。
    • 安全性:敏感数据和逻辑(如 API 密钥、数据库凭证)可以安全地保留在服务器上,永远不会泄露到客户端。
    • 自动代码分割:Next.js 会自动按组件进行代码分割,你无需手动配置。

b. 客户端组件 (Client Components)

为了实现交互性(如点击事件、状态管理 useState, useEffect 等),你需要使用客户端组件。

  • 如何定义?:在文件的顶部添加 "use client"; 指令,即可将该文件及其导入的所有组件标记为客户端组件。
  • 黄金法则:在 Next.js 的 App Router 中,所有组件默认都是 Server Components。只有当你需要使用 Hooks、事件监听器等浏览器端才有的功能时,才将组件转换为 Client Components。尽量将客户端逻辑下沉到应用的“叶子”组件中,以保持大部分应用是 Server Components。

c. 基于文件系统的路由(但以文件夹为中心)

App Router 依然使用文件系统来定义路由,但规则有所改变。

  • 文件夹即路由:每个文件夹都定义了一个 URL 段。
  • 特殊文件约定:UI 是通过在文件夹内创建具有特定名称的文件来构建的。

3. 特殊文件约定 (Special File Conventions)

这是 App Router 的“API”,你通过创建这些文件来构建应用的各个部分。

  • app/ 目录:所有 App Router 的相关文件都必须放在 app 目录下。

  • page.tsx / page.js: 定义一个路由段的 唯一 UI。这是用户访问该 URL 时看到的主要内容。一个文件夹里必须有一个 page.tsx 才能让这个路由可以被公开访问。

  • layout.tsx / layout.js: 定义一个路由段及其 子路由共享的 UIlayout 会包裹 page 或子 layout。最顶层的 app/layout.tsx 是根布局,必须包含 <html><body> 标签。布局在页面切换时 不会 重新渲染,状态会得以保留。

  • template.tsx / template.js: 与 layout 类似,也是包裹子路由的共享 UI。但关键区别在于,每次导航时 template 都会创建一个新的实例,这意味着它的状态不会被保留,并且其中的 useEffect 等会重新执行。适用于需要每次进入都执行动画或逻辑的场景。

  • loading.tsx / loading.js: 一个自动的 加载 UI。当 page.tsx 或其子组件正在获取数据时,Next.js 会使用 React Suspense 自动将 loading.tsx 的内容展示出来。这极大地简化了加载状态的管理。

  • error.tsx / error.js: 一个自动的 错误 UI。当该路由段或其子组件中抛出错误时,Next.js 会使用 React Error Boundary 自动捕获并渲染 error.tsx 的内容。它提供了一个 reset 函数,可以尝试重新渲染。

  • not-found.tsx / not-found.js: 当你在组件中调用 notFound() 函数或者访问一个不存在的路由时,会渲染这个 UI。

  • route.ts / route.js: 用于创建 API 端点,替代了 Pages Router 中的 pages/api 目录。你可以在其中导出 GET, POST, PUT, DELETE 等函数。

示例文件结构:

app/ ├── layout.tsx # 根布局 (<html>, <body>) ├── page.tsx # 首页 (/) └── dashboard/ ├── layout.tsx # Dashboard 共享布局 (例如带侧边栏) ├── page.tsx # Dashboard 主页 (/dashboard) ├── loading.tsx # /dashboard 及其子路由的加载 UI ├── error.tsx # /dashboard 及其子路由的错误 UI └── settings/ ├── page.tsx # 设置页 (/dashboard/settings)

渲染顺序:对于 /dashboard/settings,组件的嵌套关系是: app/layout.tsx -> app/dashboard/layout.tsx -> app/dashboard/settings/page.tsx


4. App Router 的关键特性与优势

a. 简化的数据获取

你可以直接在 Server Component 中使用 async/await 来获取数据。

// app/posts/[id]/page.tsx - 这是一个 Server Component async function getPost(id) { const res = await fetch(`https://api.example.com/posts/${id}`); return res.json(); } export default async function PostPage({ params }) { const post = await getPost(params.id); // 直接在组件中获取数据 return ( <div> <h1>{post.title}</h1> <p>{post.body}</p> </div> ); }

Next.js 扩展了原生的 fetch API,为其增加了自动缓存和重新验证的控制能力,你可以通过 { cache: 'no-store' }{ next: { revalidate: 3600 } } 来精细控制缓存策略。

b. 流式渲染与 Suspense (Streaming & Suspense)

这是提升用户体验的利器。当一个页面包含多个需要加载数据的组件时,服务器不必等待所有数据都准备好才开始发送响应。

  1. 服务器首先发送页面的静态部分(如布局和不需要数据的组件)。
  2. 对于正在加载数据的组件,服务器会发送一个占位符(由 loading.tsx 定义)。
  3. 一旦某个组件的数据准备就绪,服务器会通过同一个请求流,将该组件的渲染结果发送到客户端。
  4. 客户端 React 将接收到的内容无缝地填充到对应的位置。

用户会先看到页面的骨架,然后内容会逐步填充进来,大大减少了可感知的加载时间。

c. 嵌套布局与路由组 (Nested Layouts & Route Groups)

  • 嵌套布局:如上文文件结构所示,layout.tsx 文件天然支持嵌套,子路由的布局会自动包裹在父路由的布局之内,非常直观。
  • 路由组:如果你想组织文件结构,但又不希望文件夹名称影响 URL,可以使用括号 ()。例如 app/(marketing)/about/page.tsx 的 URL 仍然是 /about。这对于按功能或团队划分代码非常有用。

d. 更高级的路由模式

  • 并行路由 (Parallel Routes):允许你在同一个视图中同时渲染一个或多个页面。这对于实现复杂的仪表盘(如一个主内容区和一个侧边分析面板)非常有用。通过 @folder 语法实现。
  • 拦截路由 (Intercepting Routes):允许你从一个路由中“拦截”并显示另一个路由的内容,而 URL 保持不变。常用于在当前页面上打开一个模态框(Modal),但如果直接刷新页面,则会显示模态框的独立页面。通过 (..)(...) 语法实现。

5. App Router vs. Pages Router (快速对比)

特性App Router (app/)Pages Router (pages/)
默认组件React Server Components (RSC)客户端组件
路由文件page.tsxindex.tsx[slug].tsx
布局layout.tsx (原生支持嵌套)_app.js + getLayout 模式
数据获取async/await 在组件中, fetch 扩展getServerSideProps, getStaticProps
API 路由route.tspages/api/*.ts
加载状态loading.tsx (自动, 基于 Suspense)手动实现
错误处理error.tsx (自动, 基于 Error Boundary)手动实现或使用 _error.js
服务端渲染支持 Streaming, 默认服务器中心支持 SSR/SSG,默认客户端水合

总结

Next.js 的 App Router 是一次重大的架构升级,它将 服务器优先 的理念贯彻到底。

  • 对于开发者:它提供了更直观的布局系统、更灵活的数据获取方式,并简化了加载和错误状态的管理。虽然初期有一定的学习曲线(尤其是理解 Server/Client Components 的区别),但长期来看能显著提高开发效率和代码质量。
  • 对于用户:通过 Server Components 和流式渲染,用户可以体验到更快的页面加载速度和更流畅的交互,即使是在网络状况不佳的情况下。

虽然 Pages Router 依然被支持,但 App Router 代表了 Next.js 的未来方向。对于新项目,强烈建议从 App Router 开始。

高级特性

1. 并行路由 (Parallel Routes)

是什么? 并行路由允许你在同一个视图(由同一个 layout.tsx 控制)中,同时渲染一个或多个独立的、可独立导航的“页面”。它们就像是同一个布局下的多个“子视图”或“槽位 (slots)”。

为什么有用? 想象一个复杂的仪表盘(Dashboard)。主内容区旁边可能有一个团队活动信息流,底部还有一个分析图表。这三个区域的数据来源、加载状态、甚至交互逻辑都是独立的。

  • 传统方法:你需要在顶层 Dashboard 页面 fetch所有数据,然后通过 props 层层传递下去,非常笨拙,并且会产生请求瀑布。
  • 并行路由方法:你可以将每个区域定义为一个独立的并行路由。它们会并行加载数据,互不干扰。主布局只需要提供“插槽”,Next.js 会自动将渲染好的内容填入。

如何实现? 通过在文件夹名前加上 @ 符号来定义一个槽 (slot)。

示例文件结构:

app/ └── dashboard/ ├── @analytics/ # "analytics" 槽 │ └── page.tsx ├── @team/ # "team" 槽 │ └── page.tsx ├── layout.tsx # 共享布局 └── page.tsx # 主要内容 (隐式的 @children 槽)

app/dashboard/layout.tsx 的实现: 布局组件会通过 props 接收到所有定义的槽。

// app/dashboard/layout.tsx export default function DashboardLayout({ children, team, analytics }) { // `children` 是 page.tsx // `team` 是 @team/page.tsx // `analytics` 是 @analytics/page.tsx return ( <div> {children} <div style={{ display: 'flex', gap: '1rem' }}> {team} {analytics} </div> <div/> ); }

关键点:

  • 独立性:每个槽 (@analytics, @team) 都有自己的加载和错误状态。你可以在 @analytics/ 文件夹下添加 loading.tsxerror.tsx,它们只对该槽生效。
  • default.tsx:当 Next.js 无法根据当前 URL 确定某个槽中应渲染什么内容时(例如,在页面刷新后),它会渲染该槽下的 default.tsx 文件。这是一个很好的回退机制。

2. 拦截路由 (Intercepting Routes)

是什么? 拦截路由允许你在当前布局中“拦截”并显示一个路由的内容,同时浏览器的 URL 可以选择性地更新。用户感觉像是在当前页面上打开了一个模态框(Modal)或进行了内容预览,但这个模态框本身也是一个可以被直接访问和刷新的独立页面。

为什么有用? 最经典的案例是图片库:

  1. 用户在图片墙页面 (/gallery)。
  2. 点击一张图片,弹出一个模态框显示大图。URL 变为 /photo/123,但背景仍然是图片墙。
  3. 用户关闭模态框,URL 回到 /gallery
  4. 如果用户直接访问或刷新 /photo/123,他们会看到一个独立的、包含这张图片的完整页面,而不是模态框。

如何实现? 通过 (.)(..)(...) 这样的相对路径约定。

  • (.):匹配同一层级的路由。
  • (..):匹配上一层级的路由。
  • (..)(..):匹配上两层级的路由。
  • (...):从根 app/ 目录开始匹配。

示例文件结构(结合并行路由实现模态框):

app/ ├── @modal/ # 一个用于显示模态框的并行路由槽 │ └── (..)photo/[id]/ # 拦截规则文件夹 │ └── page.tsx # 模态框中显示的内容 ├── gallery/ │ └── page.tsx # 图片墙页面,包含指向 /photo/[id] 的链接 └── photo/ └── [id]/ └── page.tsx # 图片的独立页面 └── layout.tsx # 根布局,包含 @modal 槽

app/layout.tsx

export default function RootLayout({ children, modal }) { return ( <html> <body> {children} {modal} {/* 当拦截路由被触发时,@modal/page.tsx 的内容会渲染在这里 */} </body> </html> ); }

工作流程:

  1. 当你在 /gallery 页面点击一个 <Link href="/photo/123">
  2. Next.js 路由器检测到这个导航。
  3. 它发现 app/@modal/(..)photo/[id] 这个拦截路由匹配 photo/[id].. 表示从 gallery 的父级,即 app 开始查找)。
  4. 于是,它不会导航到 photo/[id]/page.tsx,而是渲染 app/@modal/(..)photo/[id]/page.tsx 的内容到 @modal 槽中,并更新 URL。
  5. 如果你直接刷新 /photo/123,没有发生拦截,Next.js 会正常渲染 app/photo/[id]/page.tsx

3. 服务端操作 (Server Actions)

是什么? Server Actions 是一些可以直接从客户端组件(或服务端组件)中调用的异步函数,它们 只在服务器上执行。这是一种用于数据变更(Mutations)的内置解决方案,无需手动创建 API 端点。

为什么有用? 极大地简化了表单提交和数据更新的逻辑。

  • 告别 API 路由样板代码:你不再需要为每个表单创建一个 POST /api/form 路由。
  • 渐进增强:绑定到 <form> 的 Server Action 即使在 JavaScript 被禁用的情况下也能工作。
  • 简化数据刷新:可以在 Action 内部直接调用 revalidatePathrevalidateTag 来清除缓存并刷新页面数据,实现了无缝的 UI 更新。

如何实现? 在函数顶部或文件顶部添加 "use server"; 指令。

示例:一个简单的待办事项表单

// app/actions.ts (推荐将 Actions 放在单独的文件中) "use server"; import { revalidatePath } from 'next/cache'; import { db } from './lib/db'; // 假设这是你的数据库实例 export async function addTodo(formData: FormData) { const todo = formData.get("todo") as string; if (!todo) return; await db.todos.create({ text: todo }); // 关键:当 action 执行成功后,让所有访问 /todos 的页面数据重新生效 revalidatePath("/todos"); }
// app/todos/page.tsx (可以是 Server Component) import { addTodo } from "@/app/actions"; import { db } from '@/app/lib/db'; export default async function TodosPage() { const todos = await db.todos.findMany(); return ( <div> <form action={addTodo}> {/* 直接绑定 Server Action */} <input type="text" name="todo" /> <button type="submit">Add Todo</button> </form> <ul> {todos.map(todo => <li key={todo.id}>{todo.text}</li>)} </ul> </div> ); }

4. 精细的缓存与数据重新验证 (Caching & Revalidation)

是什么? App Router 对原生的 fetch API 进行了扩展,赋予了它强大的缓存控制能力。你可以决定数据的缓存策略,并按需(On-demand)或按时(Time-based)使其失效。

为什么有用? 这是性能优化的核心。你可以灵活地在静态(SSG)、增量静态再生(ISR)和动态(SSR)渲染模式之间切换,甚至在单个页面上混合使用。

如何实现?

缓存策略 (fetch 选项):

  • 默认 (SSG/ISR): fetch('...')fetch('...', { cache: 'force-cache' })。数据在构建时被获取和缓存。
  • 动态 (SSR): fetch('...', { cache: 'no-store' })。每次请求都重新获取数据。
  • 增量静态再生 (ISR): fetch('...', { next: { revalidate: 3600 } })。数据被缓存,但在 3600 秒后第一次请求时会重新获取。

按需重新验证 (On-demand Revalidation):

  • 基于标签 (Tag-based):

    1. 在 fetch 时给数据打上标签:
      fetch('...', { next: { tags: ['posts', 'user-profile'] } });
    2. 在 Server Action 或 API 路由中使标签失效:
      // 在 Server Action 中 import { revalidateTag } from 'next/cache'; revalidateTag('posts');

    这会使所有打了 posts 标签的 fetch 数据缓存失效。

  • 基于路径 (Path-based):

    import { revalidatePath } from 'next/cache'; revalidatePath('/blog/[slug]', 'page'); // 精确重新验证单个页面 revalidatePath('/blog', 'layout'); // 重新验证布局和页面

这些高级特性共同构成了 App Router 的强大能力,它们将前端开发从“构建客户端单页应用”的思维模式,转变为“在服务器上组合可交互的 UI 片段”,从而在性能、开发体验和应用架构上都带来了质的飞跃。

约定规范

核心原则:基于文件系统,但以“约定”为中心

与 Pages Router 不同(文件名直接映射到 URL),App Router 的核心是 在特定文件夹内创建具有特殊名称的文件。文件夹定义了 URL 段,而这些特殊文件则定义了该 URL 段的 UI、逻辑或元数据。

所有 App Router 的文件和文件夹都必须位于项目根目录下的 app/ 目录中。


1. 路由定义 (Route Definition)

a. 文件夹 (Folders)

  • 作用:定义路由段 (Route Segments)。
  • 示例
    • app/dashboard/settings 会创建一个 URL 路径 /dashboard/settings
    • 嵌套文件夹会创建嵌套的路由。

b. page.tsx / page.js

  • 作用定义路由的公开 UI。这是让一个 URL 路径可以被用户直接访问的必要文件。
  • 行为:当用户访问 app/dashboard 对应的 /dashboard URL 时,app/dashboard/page.tsx 的内容会被渲染。
  • 关键点:一个文件夹(路由段)如果没有 page.tsx,它本身就不是一个可访问的页面,但它可以包含子路由。

2. UI 嵌套与布局 (UI Nesting & Layouts)

a. layout.tsx / layout.js

  • 作用:定义一个 共享的、持久化的 UI,它会包裹其所在的路由段以及所有子路由段。
  • 行为
    • 布局接收一个 children prop,代表子 layoutpage
    • 在导航切换时,layout 组件的实例和状态(state)会被保留,不会重新渲染
    • 根布局 (app/layout.tsx) 是必需的,并且必须包含 <html><body> 标签。
  • 示例app/dashboard/layout.tsx 会包裹 app/dashboard/page.tsxapp/dashboard/settings/page.tsx 等所有 /dashboard 下的页面。

b. template.tsx / template.js

  • 作用:与 layout 类似,也是一个包裹子组件的共享 UI。
  • 行为
    • 关键区别:每次导航到或离开其作用域的路由时,template 都会创建一个 新的实例
    • 这意味着它的状态不会被保留,并且 useEffectuseState 等 Hooks 会重新执行。
  • 适用场景
    • 需要实现进入/退出动画(例如使用 Framer Motion)。
    • 依赖 useEffect 来执行某些每次进入页面都需要运行的逻辑(如页面浏览量跟踪)。
    • 重置某些特定状态。

3. 加载与错误处理 (Loading & Error Handling)

a. loading.tsx / loading.js

  • 作用:创建一个 即时的加载 UI (Instant Loading UI)
  • 行为:Next.js 会自动使用 React Suspense 包裹你的 page.tsx 和其子组件。当这些组件正在进行异步操作(如 fetch 数据)时,Next.js 会自动渲染同级或上级最近的 loading.tsx 文件作为后备 (fallback)。
  • 优势:极大地简化了加载状态管理,并支持流式渲染(Streaming)。

b. error.tsx / error.js

  • 作用:创建一个 错误 UI 边界 (Error UI Boundary)
  • 行为
    • Next.js 会自动使用 React Error Boundary 包裹你的 page.tsx 和其子组件。
    • 当这些组件在渲染过程中抛出错误时,Next.js 会捕获该错误并渲染同级或上级最近的 error.tsx
    • error.tsx 组件 必须是客户端组件 ("use client";),因为它需要处理事件(如重试)。它会接收 errorreset 两个 props。reset 是一个函数,调用它可以尝试重新渲染出错的组件。
  • 优势:隔离错误,防止单个组件的错误导致整个应用崩溃。

c. global-error.tsx / global-error.js

  • 作用:这是 error.tsx 的一个特殊变体,专门用于捕获并处理 layout.tsx 中的错误。
  • 行为:由于根布局的错误会导致整个应用无法渲染,global-error.tsx 提供了一个最终的保障。它会替换整个根布局来显示错误信息。

4. 路由处理与特殊页面 (Route Handling & Special Pages)

a. not-found.tsx / not-found.js

  • 作用:定义一个 “未找到” UI
  • 行为:当 notFound() 函数在组件中被调用,或者用户访问了一个不存在的 URL 时,Next.js 会渲染最近的 not-found.tsx 文件。
  • notFound() 函数:可以在 Server Component 的数据获取逻辑中使用。如果 fetch 返回 404,你可以调用 notFound() 来中断渲染并显示 404 页面。

b. route.ts / route.js

  • 作用:创建 API 端点 (API Endpoints),替代了 Pages Router 中的 pages/api
  • 行为:你可以在此文件中导出与 HTTP 方法同名的函数,如 GET, POST, PUT, DELETE 等。
  • 示例app/api/users/route.ts 中导出的 GET 函数将处理 GET /api/users 请求。

5. 路由组织与高级模式 (Route Organization & Advanced Patterns)

a. 路由组 (Route Groups) - (folderName)

  • 作用组织文件结构,但不影响 URL 路径
  • 行为:用括号包裹的文件夹名会被路由器忽略。
  • 示例app/(marketing)/about/page.tsx 对应的 URL 是 /about,而不是 /(marketing)/about
  • 适用场景
    • 按功能或团队划分代码。
    • 为不同的路由段创建不同的根布局。例如,app/(marketing)/layout.tsxapp/(app)/layout.tsx 可以为网站的市场部分和应用部分提供完全不同的顶层布局。

b. 动态路由段 (Dynamic Segments) - [folderName]

  • 作用:匹配动态的 URL 参数。
  • 行为:与 Pages Router 类似。
  • 示例
    • app/blog/[slug]/page.tsx 会匹配 /blog/hello-world/blog/another-post 等。slug 参数会通过 props 传递给 page, layout 等组件。
  • generateStaticParams 函数:可以与动态路由段一起使用,在构建时预渲染一组特定的路径(SSG)。

c. 捕获所有段 (Catch-all Segments) - [...folderName]

  • 作用:匹配从该点开始的所有后续 URL 段。
  • 示例app/shop/[...slug]/page.tsx 会匹配 /shop/a/shop/a/b/shop/a/b/c 等。slug 参数会是一个数组,如 ['a', 'b', 'c']

d. 可选捕获所有段 (Optional Catch-all Segments) - [[...folderName]]

  • 作用:与 Catch-all 类似,但它也匹配没有后续段的路径。
  • 示例app/shop/[[...slug]]/page.tsx 会匹配 /shop 以及 /shop/a/shop/a/b 等。

e. 并行路由槽 (Parallel Route Slots) - @folderName

  • 作用:在同一个布局中定义可以独立渲染的“槽位”。
  • 示例app/dashboard/@team/page.tsx 定义了一个名为 team 的槽。父级布局 app/dashboard/layout.tsx 会通过 props 接收到 team 的渲染结果。

f. 拦截路由 (Intercepting Routes) - (.), (..)

  • 作用:在当前布局中显示另一个路由的内容,通常用于模态框。
  • 示例app/@modal/(.)photo/[id]/page.tsx 会拦截到同一层级的 photo/[id] 导航。

6. 元数据文件 (Metadata Files)

App Router 引入了一套基于文件的元数据 API。

  • favicon.ico, apple-icon.jpg, icon.jpg:放置在 app 目录的根部,用于定义应用图标。
  • opengraph-image.jpg, twitter-image.jpg:放置在任何路由段中,为该路由生成社交媒体分享卡片图片。
  • sitemap.ts / robots.ts:放置在 app 根目录,用于动态生成 sitemap.xmlrobots.txt

总结表格

文件/文件夹约定目的
app/App Router 的根目录
folder创建一个 URL 路由段
page.tsx创建一个路由段的公开 UI
layout.tsx创建持久化的共享 UI
template.tsx创建重新渲染的共享 UI
loading.tsx创建加载 UI (Suspense Boundary)
error.tsx创建错误 UI (Error Boundary)
not-found.tsx创建 404 Not Found UI
route.ts创建 API 端点
(folder)路由组,不影响 URL
[folder]动态路由段
[...folder]捕获所有路由段
[[...folder]]可选的捕获所有路由段
@folder并行路由槽
(.), (..)拦截路由

理解并熟练运用这些命名约定,是掌握 Next.js App Router 的关键所在。

例子

一、路由组 (Route Groups) - (folderName)

路由组的核心价值在于 组织代码结构而不影响最终的 URL。这看似简单,但却能解决许多实际开发中的架构问题。

1. 为应用的不同部分创建独立的布局

这是路由组最经典、最强大的应用场景。一个复杂的应用通常有几个完全不同的区域,比如:

  • 市场/营销页面 (/, /about, /pricing):通常有公共的页头、页脚,设计风格偏向展示。
  • 应用主功能页面 (/dashboard, /settings):通常需要用户登录,有侧边栏、用户菜单等复杂的交互布局。
  • 认证页面 (/login, /signup):通常是居中的、极简的布局,不应包含主应用的导航栏。

没有路由组的困境: 所有页面都共享同一个根布局 (app/layout.tsx)。你不得不在根布局中写复杂的条件逻辑来判断当前路由,然后决定渲染哪个子布局。这非常混乱且难以维护。

// 👎 不推荐的写法:在根布局中写条件逻辑 export default function RootLayout({ children }) { const pathname = usePathname(); // 需要客户端组件才能获取路径 if (pathname.startsWith('/dashboard')) { return <DashboardLayout>{children}</DashboardLayout>; } else if (pathname === '/login') { return <AuthLayout>{children}</AuthLayout>; } else { return <MarketingLayout>{children}</MarketingLayout>; } }

使用路由组的优雅解决方案: 你可以创建多个路由组,每个组都有自己的顶层 layout.tsx

文件结构:

app/ ├── (marketing)/ │ ├── about/ │ │ └── page.tsx # URL: /about │ ├── pricing/ │ │ └── page.tsx # URL: /pricing │ └── layout.tsx # 只对 (marketing) 组生效的布局 ├── (app)/ │ ├── dashboard/ │ │ └── page.tsx # URL: /dashboard │ ├── settings/ │ │ └── page.tsx # URL: /settings │ └── layout.tsx # 只对 (app) 组生效的布局,可以包含登录校验 ├── (auth)/ │ ├── login/ │ │ └── page.tsx # URL: /login │ └── layout.tsx # 只对 (auth) 组生效的布局 └── layout.tsx # 全局根布局 (仅包含 <html>, <body>)

优势:

  • 关注点分离:每个部分的布局逻辑都封装在自己的 layout.tsx 中。
  • 代码清晰:根布局保持干净,只负责最基础的 HTML 结构。
  • URL 纯净(marketing)(app) 等文件夹名不会出现在最终的 URL 中。
  • 服务端优先:布局的选择在服务端就已经确定,无需在客户端进行判断。
2. 组织项目文件,便于团队协作

当项目变大时,app 目录可能会变得非常庞大。路由组可以帮助你根据功能、特性或团队来组织文件。

文件结构:

app/ ├── (shop)/ │ ├── products/ │ ├── cart/ │ └── checkout/ ├── (blog)/ │ ├── [slug]/ │ └── layout.tsx ├── (account)/ │ ├── profile/ │ └── orders/ └── page.tsx

即使这些部分可能共享同一个主布局,这种组织方式也能让开发者快速定位到自己负责的代码区域,提高可维护性。

3. 避免将特定路由段纳入布局

有时,你希望某个页面不应用其父级的布局。路由组可以帮你“跳出”布局的包裹。

场景: 假设 app/dashboard/layout.tsx 提供了一个带侧边栏的布局。但你希望 /dashboard/reports/full 这个页面是全屏的,没有侧边栏。

文件结构:

app/ ├── dashboard/ │ ├── (with-sidebar)/ # 把需要侧边栏的页面都放进这个组 │ │ ├── settings/page.tsx │ │ └── page.tsx │ │ └── layout.tsx # 带侧边栏的布局 │ │ │ └── reports/ │ └── full/ │ └── page.tsx # 这个页面不会应用 (with-sidebar) 的布局 └── layout.tsx # dashboard 的根布局,可以更简单

通过这种方式,你可以精确控制布局的应用范围。

Last updated on